﻿
<!DOCTYPEhtml>
<html>
<head>
  <meta charset="UTF-8">
  <title>视频会议</title>
  <style>
    #container{display:grid;grid-template-columns:repeat(auto-fill,300px);gap:10px;}
    video{width:100%;height:auto;background:#000;}
  </style>
</head>
<body>
<input type="text"id="roomId"placeholder="房间号">
<input type="text"id="username"placeholder="用户名">
<button id="joinBtn">加入会议</button>
<button id="leaveBtn"disabled>离开会议</button>
<div id="container">
  <video id="localVideo"autoplay muted></video>
</div>
<div id="chat"style="margin-top:20px;">
  <input type="text"id="msgInput"placeholder="输入消息">
  <button id="sendBtn">发送</button>
  <div id="messages"style="height:200px;overflow-y:auto;border:1px solid #ccc;padding:10px;"></div>
</div>
<script>
  host="8.137.20.68"
  const config={iceServers:[{urls:"stun:117.50.221.81:3478"},
      {
        urls: "turn:117.50.221.81:3478",
        username: "username",         // ✅ 仅用户名
        credential: "password"  // ✅ 与服务器配置一致
      }
    ]};
  let websocket,localStream,peers=new Map(),remoteVideos=new Map(),currentRoom,currentUser;

  document.getElementById('joinBtn').onclick=async()=>{
    currentRoom=document.getElementById('roomId').value.trim();
    currentUser=document.getElementById('username').value.trim();
    if(!currentRoom||!currentUser)return alert('房间号和用户名不能为空');
    try{
      localStream=await navigator.mediaDevices.getUserMedia({video:true,audio:true});
      document.getElementById('localVideo').srcObject=localStream;
      websocket=new WebSocket(`wss://${host}/Web/VideoConferenceServer?roomId=${encodeURIComponent(currentRoom)}&username=${encodeURIComponent(currentUser)}`);
      websocket.onopen=()=>{
        document.getElementById('joinBtn').disabled=true;
        document.getElementById('leaveBtn').disabled=false;
        websocket.send(JSON.stringify({type:"participants_list"}));
      };
      websocket.onmessage=handleMessage;
      websocket.onerror=(e)=>console.error("WebSocket错误:",e);
      websocket.onclose=()=>console.log("WebSocket连接关闭");
    }catch(e){alert('媒体设备访问失败:'+e);}
  };

  document.getElementById('leaveBtn').onclick=()=>{
    peers.forEach((pc,user)=>{
      pc.close();
      const video=remoteVideos.get(user);
      if(video)video.remove();
    });
    peers.clear();
    remoteVideos.clear();
    localStream.getTracks().forEach(track=>track.stop());
    document.getElementById('localVideo').srcObject=null;
    websocket.send(JSON.stringify({type:"user_leave"}));
    websocket.close();
    document.getElementById('joinBtn').disabled=false;
    document.getElementById('leaveBtn').disabled=true;
  };

  document.getElementById('sendBtn').onclick=()=>{
    const msg=document.getElementById('msgInput').value.trim();
    if(msg&&websocket){
      websocket.send(JSON.stringify({type:"chat",msg}));
      document.getElementById('msgInput').value='';
    }};

  function handleMessage(event){
    const data=JSON.parse(event.data);
    switch(data.type){
      case"participants_list":
        data.users.forEach(user=>user!==currentUser&&createPeer(user));
        updateParticipants(data.users);
        break;
      case"user_join":
        if(data.fromUser!==currentUser){
          createPeer(data.fromUser);
          updateParticipants([...peers.keys(),data.fromUser]);
        }
        break;
      case"user_leave":
        handleUserLeave(data.fromUser);
        break;
      case"offer":
        handleOffer(data);
        break;
      case"answer":
        handleAnswer(data);
        break;
      case"ice_candidate":
        handleIceCandidate(data);
        break;
      case"chat":
        appendMessage(data.fromUser,data.msg);
        break;
    }}

  function createPeer(targetUser){
    if(peers.has(targetUser)||targetUser===currentUser)return;
    const pc=new RTCPeerConnection(config);
    pc.onicecandidate=e=>{
      if(e.candidate){
        websocket.send(JSON.stringify({
          type:"ice_candidate",
          toUser:targetUser,
          candidate:e.candidate.candidate,
          sdpMid:e.candidate.sdpMid,
          sdpMLineIndex:e.candidate.sdpMLineIndex
        }));
      }};
    pc.ontrack=e=>{
      const stream=e.streams[0];
      if(!remoteVideos.has(targetUser)){
        const video=document.createElement('video');
        video.autoplay=true;
        video.srcObject=stream;
        remoteVideos.set(targetUser,video);
        document.getElementById('container').appendChild(video);
      }};
    localStream.getTracks().forEach(track=>pc.addTrack(track,localStream));
    peers.set(targetUser,pc);
    if(currentUser<targetUser){
      pc.createOffer().then(offer=>{
        pc.setLocalDescription(offer);
        websocket.send(JSON.stringify({type:"offer",toUser:targetUser,sdp:offer.sdp}));
      });}}

  async function handleOffer(data){
    const pc=peers.get(data.fromUser)||createPeer(data.fromUser);
    await pc.setRemoteDescription(new RTCSessionDescription({type:"offer",sdp:data.sdp}));
    const answer=await pc.createAnswer();
    await pc.setLocalDescription(answer);
    websocket.send(JSON.stringify({type:"answer",toUser:data.fromUser,sdp:answer.sdp}));
  }

  async function handleAnswer(data){
    const pc=peers.get(data.fromUser);
    if(pc)await pc.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:data.sdp}));
  }

  function handleIceCandidate(data){
    const pc=peers.get(data.fromUser);
    if(pc)pc.addIceCandidate(new RTCIceCandidate(data));
  }

  function handleUserLeave(user){
    const pc=peers.get(user);
    if(pc){
      pc.close();
      peers.delete(user);
      const video=remoteVideos.get(user);
      if(video)video.remove();
      remoteVideos.delete(user);
      updateParticipants([...peers.keys()]);
    }}

  function updateParticipants(users){
    document.getElementById('messages').innerHTML+=
            `<div style="color:gray">当前参会者:${users.join(',')}</div>`;
  }
  function appendMessage(user,msg){
    document.getElementById('messages').innerHTML+=
            `<div><strong>${user}:</strong> ${msg}</div>`;
    document.getElementById('messages').scrollTop=document.getElementById('messages').scrollHeight;
  }
</script>
</body>
</html>